﻿/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

#pragma warning disable 420

using System;
using System.Threading;

namespace zeal.util
{
    public interface IProgressMonitor
    {
        string task { get; }
        int totalWork { get; }
        int workComplete { get; }
        int workRemaining { get; }
        double percentComplete { get; }
        bool started { get; }
        bool completed { get; }
        bool cancellable { get; }
        bool cancelled { get; }
        void cancel();
    }

    public interface IProgressReporter : IProgressMonitor
    {
        new string task { set; }
        void begin(string task, int totalWork);
        void worked(int ammount);
        void done();
        IProgressReporter subTask(int workFromTotal, bool propogateTaskName);
    }

    public class ConcurrentProgressReporter : IProgressReporter
    {
        private readonly bool _cancellable;
        private volatile bool _cancelled;
        private volatile int _totalWork = -1;
        private volatile int _workComplete;
        private volatile string _task = string.Empty;

        public ConcurrentProgressReporter(bool cancellable) { _cancellable = cancellable; }

        public void begin(string task, int totalWork)
        {
            if (task == null) task = string.Empty;
            if(totalWork < 0) throw new ArgumentOutOfRangeException("totalWork", "totalWork must be >= 0");
            if(Interlocked.CompareExchange(ref _totalWork, totalWork, -1) != -1)
                throw new InvalidOperationException("The progress reporter has already been started");
            _task = task;
        }

        public virtual void worked(int ammount)
        {
            if(ammount <= 0) return;
            int tw = _totalWork;
            if(tw < 0) throw new InvalidOperationException("The progress reporter has not been started");
            Interlocked.Add(ref _workComplete, ammount);
        }

        public IProgressReporter subTask(int workFromTotal, bool propogateTaskName)
        {
            int tw = _totalWork;
            if(tw < 0) throw new InvalidOperationException("The progress reporter has not been started");
            if(workFromTotal < 1) throw new ArgumentOutOfRangeException("workFromTotal", "workFromTotal must be at least 1");
            return new SubProgressReporter(this, workFromTotal, propogateTaskName);
        }

        public void done()
        {
            int tw = _totalWork;
            int wc = _workComplete;
            if(wc < tw) worked(tw - wc);
        }

        public virtual void cancel()
        {
            if(!_cancellable) throw new InvalidOperationException("The progress monitor is not cancellable");
            _cancelled = true;
        }

        public virtual string task { get { return _task; } set { _task = value ?? string.Empty; } }
        public int totalWork { get { return _totalWork; } }
        public int workComplete { get { int tw = _totalWork; return tw < 0 ? 0 : _workComplete; } }
        public int workRemaining { get { int tw = _totalWork; int wc = _workComplete; return tw < 0 ? 0 : Math.Max(0, tw - wc); } }
        public double percentComplete { get { int tw = _totalWork; int wc = _workComplete; return tw < 0 ? 0 : Math.Min(1d, (double) wc / tw); } }
        public bool started { get { int tw = _totalWork; return tw > 0; } }
        public bool completed { get { int tw = _totalWork; int wc = _workComplete; return tw > 0 && wc >= tw; } }
        public bool cancellable { get { return _cancellable; } }
        public bool cancelled { get { return _cancelled; } }

        private sealed class SubProgressReporter : ConcurrentProgressReporter
        {
            private readonly ConcurrentProgressReporter _pm;
            private readonly int _workFromTotal;
            private readonly bool _propogateName;
            private volatile int _completedPmWork;

            public SubProgressReporter(ConcurrentProgressReporter pm, int workFromTotal, bool propogateName) : base(pm.cancellable)
            {
                _pm = pm;
                _workFromTotal = workFromTotal;
                _propogateName = propogateName;
            }

            public override void worked(int ammount)
            {
                if(ammount <= 0) return;
                int tw = _totalWork;
                if(tw < 0) throw new InvalidOperationException("The progress reporter has not been started");
                int wc = Interlocked.Add(ref _workComplete, ammount);
                
                while(true)
                {
                    if(wc >= tw) return;
                    double ratio = (double) wc / tw;
                    int targetWork = (int)(ratio * _workFromTotal);
                    int cpw = Thread.VolatileRead(ref _completedPmWork);
                    int workAmmount = targetWork - cpw;
                    if(workAmmount <= 0) return;
                    int newCpw = cpw + workAmmount;
                    if(Interlocked.CompareExchange(ref _completedPmWork, newCpw, cpw) == cpw)
                    {
                        _pm.worked(workAmmount);
                        return;
                    }
                    wc = Thread.VolatileRead(ref _workComplete);
                }
            }

            public override void cancel() { base.cancel(); _pm.cancel(); }
            public override string task {  get { return _task; } set { _task = value; if(_propogateName) _pm.task = value; } }
        }
    }
}